系列文章: 前端工程師的 Modern Web 實踐之道 - Day 10
預計閱讀時間: 10 分鐘
難度等級: ⭐⭐⭐☆☆
在前一篇文章中,我們探討了現代化模組架構的設計原則。今天我們將深入討論前端開發中最讓人頭痛的問題之一:狀態管理。從早期的 jQuery 全域變數,到 Redux 的嚴格規範,再到現代化的 Zustand、Jotai 等輕量解決方案,狀態管理技術正在經歷一場革命。
還記得那些年我們用 jQuery 寫的「義大利麵條式」程式碼嗎?全域變數滿天飛,狀態散布在各個 DOM 元素的 data 屬性中,一個 bug 要追查半天。
// 傳統 jQuery 的狀態管理噩夢
var userInfo = {};
var cartItems = [];
var isLoading = false;
$('#login-btn').click(function() {
isLoading = true;
$('.spinner').show();
// 誰知道這些狀態會在哪裡被修改?
});
現代前端開發中,我們面臨的狀態管理挑戰包括:
Redux 基於 Flux 架構,強制執行單向資料流,確保狀態變更的可預測性。
// Redux 的經典實作
import { createStore, combineReducers } from 'redux';
// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const SET_USER = 'SET_USER';
// Action Creators
const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT });
const setUser = (user) => ({ type: SET_USER, payload: user });
// Reducers
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case INCREMENT:
return { ...state, count: state.count + 1 };
case DECREMENT:
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const userReducer = (state = { user: null }, action) => {
switch (action.type) {
case SET_USER:
return { ...state, user: action.payload };
default:
return state;
}
};
// Store
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer
});
const store = createStore(rootReducer);
Redux 的優勢:
Redux 的痛點:
Redux Toolkit (RTK) 是 Redux 官方推薦的現代化寫法,大幅簡化了 Redux 的使用。
import { createSlice, configureStore } from '@reduxjs/toolkit';
// 使用 createSlice 簡化 Redux 邏輯
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment: (state) => {
// 使用 Immer 讓我們可以直接「修改」狀態
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
incrementByAmount: (state, action) => {
state.count += action.payload;
}
}
});
const userSlice = createSlice({
name: 'user',
initialState: { user: null, loading: false, error: null },
reducers: {
setLoading: (state, action) => {
state.loading = action.payload;
},
setUser: (state, action) => {
state.user = action.payload;
state.loading = false;
state.error = null;
},
setError: (state, action) => {
state.error = action.payload;
state.loading = false;
}
}
});
// 自動生成 action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const { setLoading, setUser, setError } = userSlice.actions;
// 設定 store
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
user: userSlice.reducer
},
// 內建最佳實踐的中介層
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
})
});
Zustand 提供了更簡潔的 API,同時保持強大的功能。
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// 基礎 store
const useCounterStore = create((set, get) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 可以直接在 store 中定義計算屬性
doubleCount: () => get().count * 2
}));
// 結合中介層的進階使用
const useUserStore = create(
devtools(
persist(
(set, get) => ({
user: null,
loading: false,
error: null,
// 非同步 action
fetchUser: async (userId) => {
set({ loading: true, error: null });
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set({ user, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
// 重設狀態
logout: () => set({ user: null, error: null }),
// 複雜的狀態計算
isAuthenticated: () => !!get().user,
userPermissions: () => get().user?.permissions || []
}),
{
name: 'user-storage', // 持久化設定
partialize: (state) => ({ user: state.user }) // 只持久化特定欄位
}
),
{ name: 'user-store' } // DevTools 名稱
)
);
// 在 React 組件中使用
function Counter() {
const { count, increment, decrement, doubleCount } = useCounterStore();
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount()}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// 選擇性訂閱,最佳化效能
function UserProfile() {
// 只訂閱 user 和 loading,其他狀態變更不會觸發重新渲染
const { user, loading, fetchUser } = useUserStore(
(state) => ({
user: state.user,
loading: state.loading,
fetchUser: state.fetchUser
})
);
return (
<div>
{loading ? (
<div>載入中...</div>
) : user ? (
<div>歡迎,{user.name}!</div>
) : (
<button onClick={() => fetchUser('123')}>載入使用者</button>
)}
</div>
);
}
Jotai 採用原子化思維,將狀態拆分為最小單位,提供更細緻的狀態控制。
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
// 基礎原子
const countAtom = atom(0);
const userAtom = atom(null);
const loadingAtom = atom(false);
// 派生原子(計算屬性)
const doubleCountAtom = atom((get) => get(countAtom) * 2);
const isAuthenticatedAtom = atom((get) => !!get(userAtom));
// 寫入原子(類似 action)
const incrementAtom = atom(
null, // 讀取函數為 null 表示這是純寫入原子
(get, set) => set(countAtom, get(countAtom) + 1)
);
// 非同步原子
const fetchUserAtom = atom(
null,
async (get, set, userId) => {
set(loadingAtom, true);
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
set(userAtom, user);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
set(loadingAtom, false);
}
}
);
// 在組件中使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
const increment = useSetAtom(incrementAtom);
return (
<div>
<p>Count: {count}</p>
<p>Double: {doubleCount}</p>
<button onClick={increment}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
function UserProfile() {
const user = useAtomValue(userAtom);
const loading = useAtomValue(loadingAtom);
const fetchUser = useSetAtom(fetchUserAtom);
return (
<div>
{loading ? (
<div>載入中...</div>
) : user ? (
<div>歡迎,{user.name}!</div>
) : (
<button onClick={() => fetchUser('123')}>載入使用者</button>
)}
</div>
);
}
讓我們透過一個實際案例來比較不同的狀態管理方案。我們要建立一個具備以下功能的購物車系統:
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// 商品管理 store
const useProductStore = create(
devtools((set, get) => ({
products: [],
loading: false,
error: null,
fetchProducts: async () => {
set({ loading: true, error: null });
try {
const response = await fetch('/api/products');
const products = await response.json();
set({ products, loading: false });
} catch (error) {
set({ error: error.message, loading: false });
}
},
getProductById: (id) => {
return get().products.find(product => product.id === id);
}
}), { name: 'product-store' })
);
// 購物車管理 store
const useCartStore = create(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (productId, quantity = 1) => {
set((state) => {
const existingItem = state.items.find(item => item.productId === productId);
if (existingItem) {
return {
items: state.items.map(item =>
item.productId === productId
? { ...item, quantity: item.quantity + quantity }
: item
)
};
} else {
return {
items: [...state.items, { productId, quantity }]
};
}
});
},
removeItem: (productId) => {
set((state) => ({
items: state.items.filter(item => item.productId !== productId)
}));
},
updateQuantity: (productId, quantity) => {
if (quantity <= 0) {
get().removeItem(productId);
return;
}
set((state) => ({
items: state.items.map(item =>
item.productId === productId
? { ...item, quantity }
: item
)
}));
},
clearCart: () => set({ items: [] }),
// 計算屬性
getTotalItems: () => {
return get().items.reduce((total, item) => total + item.quantity, 0);
},
getTotalPrice: () => {
const { getProductById } = useProductStore.getState();
return get().items.reduce((total, item) => {
const product = getProductById(item.productId);
return total + (product?.price || 0) * item.quantity;
}, 0);
}
}),
{
name: 'cart-storage',
partialize: (state) => ({ items: state.items })
}
),
{ name: 'cart-store' }
)
);
// 訂單管理 store
const useOrderStore = create(
devtools((set, get) => ({
currentOrder: null,
orderHistory: [],
processing: false,
createOrder: async () => {
const { items, clearCart } = useCartStore.getState();
const { user } = useUserStore.getState();
if (!user || items.length === 0) {
throw new Error('無法建立訂單:使用者未登入或購物車為空');
}
set({ processing: true });
try {
const orderData = {
userId: user.id,
items,
totalAmount: useCartStore.getState().getTotalPrice(),
createdAt: new Date().toISOString()
};
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orderData)
});
const order = await response.json();
set((state) => ({
currentOrder: order,
orderHistory: [order, ...state.orderHistory],
processing: false
}));
clearCart(); // 清空購物車
return order;
} catch (error) {
set({ processing: false });
throw error;
}
},
fetchOrderHistory: async () => {
const { user } = useUserStore.getState();
if (!user) return;
try {
const response = await fetch(`/api/orders/user/${user.id}`);
const orders = await response.json();
set({ orderHistory: orders });
} catch (error) {
console.error('Failed to fetch order history:', error);
}
}
}), { name: 'order-store' })
);
import React, { useEffect } from 'react';
// 商品列表組件
function ProductList() {
const { products, loading, error, fetchProducts } = useProductStore();
const addItem = useCartStore(state => state.addItem);
useEffect(() => {
fetchProducts();
}, [fetchProducts]);
if (loading) return <div>載入商品中...</div>;
if (error) return <div>錯誤:{error}</div>;
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>價格:${product.price}</p>
<button onClick={() => addItem(product.id)}>
加入購物車
</button>
</div>
))}
</div>
);
}
// 購物車組件
function Cart() {
const { items, updateQuantity, removeItem, getTotalItems, getTotalPrice } = useCartStore();
const { getProductById } = useProductStore();
const cartItems = items.map(item => ({
...item,
product: getProductById(item.productId)
})).filter(item => item.product);
return (
<div className="cart">
<h2>購物車 ({getTotalItems()} 件商品)</h2>
{cartItems.length === 0 ? (
<p>購物車是空的</p>
) : (
<>
{cartItems.map(item => (
<div key={item.productId} className="cart-item">
<span>{item.product.name}</span>
<span>${item.product.price}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.productId, parseInt(e.target.value))}
min="0"
/>
<button onClick={() => removeItem(item.productId)}>
移除
</button>
</div>
))}
<div className="cart-total">
總計:${getTotalPrice()}
</div>
<CheckoutButton />
</>
)}
</div>
);
}
// 結帳組件
function CheckoutButton() {
const { createOrder, processing } = useOrderStore();
const { user } = useUserStore();
const { items } = useCartStore();
const handleCheckout = async () => {
if (!user) {
alert('請先登入');
return;
}
if (items.length === 0) {
alert('購物車是空的');
return;
}
try {
const order = await createOrder();
alert(`訂單建立成功!訂單編號:${order.id}`);
} catch (error) {
alert(`訂單建立失敗:${error.message}`);
}
};
return (
<button
onClick={handleCheckout}
disabled={processing || !user || items.length === 0}
className="checkout-button"
>
{processing ? '處理中...' : '結帳'}
</button>
);
}
// 避免過度渲染的選擇器模式
function OptimizedProductList() {
// ❌ 錯誤:會訂閱整個 store
// const store = useProductStore();
// ✅ 正確:只訂閱需要的狀態
const products = useProductStore(state => state.products);
const loading = useProductStore(state => state.loading);
const fetchProducts = useProductStore(state => state.fetchProducts);
// 或者使用 shallow 比較
const { products, loading, fetchProducts } = useProductStore(
state => ({
products: state.products,
loading: state.loading,
fetchProducts: state.fetchProducts
}),
shallow // 需要 import { shallow } from 'zustand/shallow'
);
// 組件實作...
}
// 記憶化選擇器
const useCartSummary = () => {
return useCartStore(
useCallback((state) => ({
totalItems: state.getTotalItems(),
totalPrice: state.getTotalPrice(),
itemCount: state.items.length
}), []),
shallow
);
};
// 使用切片模式組織大型應用狀態
const createAuthSlice = (set, get) => ({
user: null,
token: null,
loading: false,
login: async (credentials) => {
set({ loading: true });
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const { user, token } = await response.json();
set({ user, token, loading: false });
} catch (error) {
set({ loading: false });
throw error;
}
},
logout: () => set({ user: null, token: null })
});
const createCartSlice = (set, get) => ({
items: [],
addItem: (productId, quantity) => { /* ... */ },
removeItem: (productId) => { /* ... */ }
});
const createUISlice = (set, get) => ({
sidebarOpen: false,
theme: 'light',
notifications: [],
toggleSidebar: () => set(state => ({ sidebarOpen: !state.sidebarOpen })),
setTheme: (theme) => set({ theme }),
addNotification: (notification) => set(state => ({
notifications: [...state.notifications, { ...notification, id: Date.now() }]
}))
});
// 組合所有切片
const useAppStore = create()(
devtools(
persist(
(...args) => ({
...createAuthSlice(...args),
...createCartSlice(...args),
...createUISlice(...args)
}),
{
name: 'app-storage',
partialize: (state) => ({
user: state.user,
token: state.token,
items: state.items,
theme: state.theme
})
}
)
)
);